<!DOCTYPE html>
<html>
  <head>
    <title>
      Test setTargetAtTime Approach to Limit
    </title>
    <script src="../../resources/testharness.js"></script>
    <script src="../../resources/testharnessreport.js"></script>
    <script src="../resources/audit-util.js"></script>
    <script src="../resources/audit.js"></script>
    <script src="../resources/audioparam-testing.js"></script>
    <script src="../resources/audio-param.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let audit = Audit.createTaskRunner();

      audit.define('approach 1', (task, should) => {
        let sampleRate = 48000;

        // A really short time constant so that setTargetAtTime approaches the
        // limiting value well before the end of the test.
        let timeConstant = 0.001;

        // Find the time where setTargetAtTime is close enough to the limit.
        // Since we're approaching 1, use a value of eps smaller than
        // kSetTargetThreshold (1.5e-6) in AudioParamTimeline.cpp.  This is to
        // account for round-off in the actual implementation (which uses a
        // filter and not the formula.)
        let limitThreshold = 1e-6;

        runTest(should, {
          sampleRate: sampleRate,
          v0: 0,
          v1: 1,
          timeConstant: timeConstant,
          eps: limitThreshold,
          // Experimentally determined
          threshold: 2.4e-5,
          tailThreshold: {relativeThreshold: 9.8234e-7}
        }).then(() => task.done());
      })

      audit.define('approach 0', (task, should) => {
        // Use the equation for setTargetAtTime to figure out when we are close
        // to 0:
        //
        //   v(t) = exp(-t/tau)
        //
        // So find t such that exp(-t/tau) <= eps.  Thus t >= - tau * log(eps).
        //
        // For eps, use exp(-10).

        let sampleRate = 48000;

        // A really short time constant so that setTargetAtTime approaches the
        // limiting value well before the end of the test.
        let timeConstant = 0.001;

        // Find the time where setTargetAtTime is close enough to the limit.
        // Since we're approaching 0, use a value of eps smaller than
        // kSetTargetZeroThreshold (1e-20) in AudioParamTimeline.cpp.  This is
        // to account for round-off in the actual implementation (which uses a
        // filter and not the formula.)
        let limitThreshold = Math.exp(-10);

        runTest(should, {
          sampleRate: sampleRate,
          v0: 1,
          v1: 0,
          timeConstant: timeConstant,
          eps: limitThreshold,
          // Experimentally determined
          threshold: 1.2591e-7,
          tailThreshold: {absoluteThreshold: 2.3310e-5}
        }).then(() => task.done());
      });

      audit.define('crbug.com/835294', (task, should) => {
        let sampleRate = 8000;
        let duration = 8;
        let context = new OfflineAudioContext({
          length: duration * sampleRate,
          sampleRate: sampleRate});

        let src = new ConstantSourceNode(context);
        let gain = new GainNode(context);

        src.connect(gain).connect(context.destination);

        let targetValue = 0.8;
        let targetStartTime = 0;
        let timeConstant = 2;
        gain.gain.setValueAtTime(0, 0);
        gain.gain.setTargetAtTime(targetValue, targetStartTime, timeConstant);
        src.start();

        context.startRendering()
          .then(renderedBuffer => {
             // The output should be a simple exponential approach to 0.8
             // starting at 0 at time 0.1.
             let actual = renderedBuffer.getChannelData(0);
             let expected = createExponentialApproachArray(targetStartTime,
               duration, 0, targetValue, context.sampleRate, timeConstant);

             should(actual, 'Output')
              .beCloseToArray(expected, {absoluteThreshold: 5.0098e-6});
          })
          .then(() => task.done());
      });

      function findLimitTime(v0, v1, timeConstant, eps) {
        // Find the time at which the setTargetAtTime is close enough to the
        // target value |v1| where we can consider the curve to have reached its
        // limiting value.
        //
        // If v1 = 0, |eps| is the absolute error between the actual value and
        // |v1|.  Otherwise, |eps| is the relative error between the actual
        // value and |v1|.
        //
        // The curve is
        //
        //   v(t) = v1 - (v1 - v0) * exp(-t/timeConstant)
        //
        // If v1 = 0,
        //
        //   v(t) = v0 * exp(-t/timeConstant)
        //
        // Solve this for when |v(t)| <= eps:
        //
        //   t >= timeConstant * log(v0/eps)
        //
        // For v1 not zero, we want |v(t) - v1|/|v1| <= eps:
        //
        //   t >= timeConstant * log(abs(v1-v0)/eps/v1)

        if (v1)
          return timeConstant * Math.log(Math.abs(v1 - v0) / eps / v1);
        else
          return timeConstant * Math.log(v0 / eps);
      }

      function runTest(should, options) {
        let renderLength = 1;

        let context = new OfflineAudioContext(
            1, renderLength * sampleRate, options.sampleRate);

        // A constant source
        let source = context.createBufferSource();
        source.buffer = createConstantBuffer(context, 1, 1);
        source.loop = true;

        let gain = context.createGain();
        gain.gain.setValueAtTime(options.v0, 0);
        gain.gain.setTargetAtTime(options.v1, 0, options.timeConstant);

        source.connect(gain);
        gain.connect(context.destination);

        source.start();

        return context.startRendering().then(function(resultBuffer) {
          let actual = resultBuffer.getChannelData(0);
          let expected = createExponentialApproachArray(
              0, renderLength, options.v0, options.v1, options.sampleRate,
              options.timeConstant);

          let message = 'setTargetAtTime(' + options.v1 + ', 0, ' +
              options.timeConstant + ')';

          // Determine where the tail of the curve begins.  (Where the curve has
          // basically reached the limit value.)
          let tailTime = findLimitTime(
              options.v0, options.v1, options.timeConstant, options.eps);
          let tailFrame = Math.ceil(tailTime * options.sampleRate);

          should(
              actual.slice(0, tailFrame),
              'Initial output of ' + tailFrame + ' samples for ' + message)
              .beCloseToArray(
                  expected.slice(0, tailFrame),
                  {absoluteThreshold: options.threshold});

          should(actual.slice(tailFrame), 'Tail output for ' + message)
              .beCloseToArray(expected.slice(tailFrame), options.tailThreshold);
        });
      }

      audit.run();
    </script>
  </body>
</html>
